{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Note: you may need to restart the kernel to use updated packages.\n",
      "Name: tensorflow-cpu\n",
      "Version: 2.15.0.post1\n",
      "Summary: TensorFlow is an open source machine learning framework for everyone.\n",
      "Home-page: https://www.tensorflow.org/\n",
      "Author: Google Inc.\n",
      "Author-email: packages@tensorflow.org\n",
      "License: Apache 2.0\n",
      "Location: /home/Clickhouse/.venv/lib/python3.9/site-packages\n",
      "Requires: absl-py, astunparse, flatbuffers, gast, google-pasta, grpcio, h5py, keras, libclang, ml-dtypes, numpy, opt-einsum, packaging, protobuf, setuptools, six, tensorboard, tensorflow-estimator, tensorflow-io-gcs-filesystem, termcolor, typing-extensions, wrapt\n",
      "Required-by: \n",
      "---\n",
      "Name: chdb\n",
      "Version: 1.0.1\n",
      "Summary: chDB is an in-process SQL OLAP Engine powered by ClickHouse\n",
      "Home-page: https://github.com/auxten/chdb\n",
      "Author: auxten\n",
      "Author-email: auxtenwpc@gmail.com\n",
      "License: Apache-2.0\n",
      "Location: /home/Clickhouse/.venv/lib/python3.9/site-packages\n",
      "Requires: \n",
      "Required-by: \n",
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "%pip install -q -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade tensorflow-cpu chdb pandas pyarrow scikit-learn numpy matplotlib\n",
    "%pip show tensorflow-cpu chdb"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "total 1129584\n",
      "-rw-r--r-- 1 root root     10460 Dec 11 08:24 README.txt\n",
      "-rw-r--r-- 1 root root 435164157 Dec 11 08:24 genome-scores.csv\n",
      "-rw-r--r-- 1 root root     18103 Dec 11 08:24 genome-tags.csv\n",
      "-rw-r--r-- 1 root root   1368578 Dec 11 08:24 links.csv\n",
      "-rw-r--r-- 1 root root   3038099 Dec 11 08:24 movies.csv\n",
      "-rw-r--r-- 1 root root 678260987 Dec 11 08:24 ratings.csv\n",
      "-rw-r--r-- 1 root root  38810332 Dec 11 08:24 tags.csv\n"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "import zipfile\n",
    "import urllib.request\n",
    "import os\n",
    "import chdb\n",
    "from chdb import session\n",
    "\n",
    "# Download and extract the dataset\n",
    "if not os.path.exists(\"ml-25m/ratings.csv\"):\n",
    "    url = \"https://files.grouplens.org/datasets/movielens/ml-25m.zip\"\n",
    "    import ssl\n",
    "    ssl._create_default_https_context = ssl._create_unverified_context\n",
    "    filehandle, _ = urllib.request.urlretrieve(url)\n",
    "    zip_file_object = zipfile.ZipFile(filehandle, \"r\")\n",
    "    zip_file_object.extractall()\n",
    "\n",
    "!ls -l ml-25m"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1,296,5,1147880044\n",
      "1,306,3.5,1147868817\n",
      "1,307,5,1147868828\n",
      "1,665,5,1147878820\n",
      "1,899,3.5,1147868510\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Peek at the data\n",
    "print(chdb.query(\"SELECT * FROM file('ml-25m/ratings.csv') LIMIT 5\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create views for the tables of movieLens dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"movieId\",\"title\",\"genres\"\n",
      "1,\"Toy Story (1995)\",\"Adventure|Animation|Children|Comedy|Fantasy\"\n",
      "2,\"Jumanji (1995)\",\"Adventure|Children|Fantasy\"\n",
      "3,\"Grumpier Old Men (1995)\",\"Comedy|Romance\"\n",
      "4,\"Waiting to Exhale (1995)\",\"Comedy|Drama|Romance\"\n",
      "5,\"Father of the Bride Part II (1995)\",\"Comedy\"\n",
      "\n",
      "\"userId\",\"movieId\",\"rating\",\"timestamp\"\n",
      "1,296,5,1147880044\n",
      "1,306,3.5,1147868817\n",
      "1,307,5,1147868828\n",
      "1,665,5,1147878820\n",
      "1,899,3.5,1147868510\n",
      "\n",
      "\"userId\",\"movieId\",\"tag\",\"timestamp\"\n",
      "3,260,\"classic\",1439472355\n",
      "3,260,\"sci-fi\",1439472256\n",
      "4,1732,\"dark comedy\",1573943598\n",
      "4,1732,\"great dialogue\",1573943604\n",
      "4,7569,\"so bad it's good\",1573943455\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Create tables for the tables of movieLens dataset\n",
    "chs = session.Session()\n",
    "chs.query(\"CREATE DATABASE IF NOT EXISTS movielens ENGINE = Atomic\")\n",
    "chs.query(\"USE movielens\")\n",
    "chs.query(\n",
    "    \"CREATE VIEW movies AS SELECT movieId, title, genres FROM file('ml-25m/movies.csv')\"\n",
    ")\n",
    "chs.query(\n",
    "    \"CREATE VIEW ratings AS SELECT userId, movieId, rating, timestamp FROM file('ml-25m/ratings.csv')\"\n",
    ")\n",
    "chs.query(\n",
    "    \"CREATE VIEW tags AS SELECT userId, movieId, tag, timestamp FROM file('ml-25m/tags.csv')\"\n",
    ")\n",
    "print(chs.query(\"SELECT * FROM movies LIMIT 5\", \"CSVWithNames\"))\n",
    "print(chs.query(\"SELECT * FROM ratings LIMIT 5\", \"CSVWithNames\"))\n",
    "print(chs.query(\"SELECT * FROM tags LIMIT 5\", \"CSVWithNames\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create a view to join the movies/ratings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"userId\",\"movieId\",\"title\",\"genres\",\"liked\"\n",
      "1,296,\"Pulp Fiction (1994)\",\"Comedy|Crime|Drama|Thriller\",1\n",
      "1,306,\"Three Colors: Red (Trois couleurs: Rouge) (1994)\",\"Drama\",0\n",
      "1,307,\"Three Colors: Blue (Trois couleurs: Bleu) (1993)\",\"Drama\",1\n",
      "1,665,\"Underground (1995)\",\"Comedy|Drama|War\",1\n",
      "1,899,\"Singin' in the Rain (1952)\",\"Comedy|Musical|Romance\",0\n",
      "\n",
      "Training rows: 19994500\n",
      "\n",
      "Test rows: 5005595\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Create a view to join the movies/ratings, if user rating >3.5 to a movie then 1(like) else 0(dislike)\n",
    "chs.query(\n",
    "    \"\"\"\n",
    "    CREATE OR REPLACE VIEW user_ratings AS\n",
    "        SELECT ratings.userId userId, ratings.movieId movieId, movies.title title, genres,\n",
    "            CASE WHEN rating > 3.5 THEN 1 ELSE 0 END AS liked\n",
    "        FROM ratings\n",
    "        JOIN movies USING movieId\n",
    "    \"\"\"\n",
    ")\n",
    "# Peek at the data\n",
    "print(chs.query(\"SELECT * FROM user_ratings LIMIT 5\", \"CSVWithNames\"))\n",
    "\n",
    "# Split the data into train and test with userId\n",
    "chs.query(\n",
    "    \"\"\"\n",
    "    CREATE OR REPLACE VIEW train AS\n",
    "        SELECT userId, movieId, title, genres, liked\n",
    "        FROM user_ratings\n",
    "        WHERE userId % 10 < 8;\n",
    "    CREATE OR REPLACE VIEW test AS\n",
    "        SELECT userId, movieId, title, genres, liked\n",
    "        FROM user_ratings\n",
    "        WHERE userId % 10 >= 8;\n",
    "    \"\"\"\n",
    ")\n",
    "# Count the number of rows in train and test\n",
    "print(\"Training rows:\", chs.query(\"SELECT COUNT(*) FROM train\"))\n",
    "print(\"Test rows:\", chs.query(\"SELECT COUNT(*) FROM test\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train a DNN model to predict if a user will like a movie or not"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "X_train sample: [1 296 'Pulp Fiction (1994)' 'Comedy|Crime|Drama|Thriller']\n",
      "y_train sample: 1\n",
      "X_val sample: [8 1 'Toy Story (1995)' 'Adventure|Animation|Children|Comedy|Fantasy']\n",
      "y_val sample: 1\n",
      "X_train sample: [1 296 'Pulp Fiction (1994)' 'Comedy|Crime|Drama|Thriller' 0.997]\n",
      "X_val sample: [8 1 'Toy Story (1995)' 'Adventure|Animation|Children|Comedy|Fantasy'\n",
      " 0.9975]\n",
      "X_train sample: [  1.    296.      0.997   0.      0.      0.      0.      0.      1.\n",
      "   1.      0.      1.      0.      0.      0.      0.      0.      0.\n",
      "   0.      0.      1.      0.   ]\n",
      "X_val sample: [8.     1.     0.9975 0.     0.     1.     1.     1.     1.     0.\n",
      " 0.     0.     1.     0.     0.     0.     0.     0.     0.     0.\n",
      " 0.     0.    ]\n",
      "Epoch 1/10\n",
      "489/489 [==============================] - 6s 10ms/step - loss: 1300839.7500 - val_loss: 388.4711\n",
      "Epoch 2/10\n",
      "489/489 [==============================] - 4s 8ms/step - loss: 209.9686 - val_loss: 135.4464\n",
      "Epoch 3/10\n",
      "489/489 [==============================] - 5s 9ms/step - loss: 112.1556 - val_loss: 95.9700\n",
      "Epoch 4/10\n",
      "489/489 [==============================] - 4s 9ms/step - loss: 86.1744 - val_loss: 76.2465\n",
      "Epoch 5/10\n",
      "489/489 [==============================] - 4s 9ms/step - loss: 68.3167 - val_loss: 59.6845\n",
      "Epoch 6/10\n",
      "489/489 [==============================] - 4s 8ms/step - loss: 53.1876 - val_loss: 46.0609\n",
      "Epoch 7/10\n",
      "489/489 [==============================] - 5s 9ms/step - loss: 40.9774 - val_loss: 35.2976\n",
      "Epoch 8/10\n",
      "489/489 [==============================] - 5s 9ms/step - loss: 30.9578 - val_loss: 26.3277\n",
      "Epoch 9/10\n",
      "489/489 [==============================] - 5s 9ms/step - loss: 22.7545 - val_loss: 19.0584\n",
      "Epoch 10/10\n",
      "489/489 [==============================] - 5s 9ms/step - loss: 5232.4829 - val_loss: 52.5246\n",
      "156425/156425 [==============================] - 125s 800us/step\n",
      "RMSE: 7.247381\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "from sklearn.metrics import mean_squared_error\n",
    "import numpy as np\n",
    "from sklearn.preprocessing import MultiLabelBinarizer\n",
    "\n",
    "# Load the data\n",
    "train_data = chs.query(\n",
    "    \"SELECT userId, movieId, title, genres, liked FROM train\", \"DataFrame\"\n",
    ")\n",
    "validate_data = chs.query(\n",
    "    \"SELECT userId, movieId, title, genres, liked FROM test\", \"DataFrame\"\n",
    ")\n",
    "\n",
    "# # Split the data into training and validation sets\n",
    "# X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n",
    "X_train = train_data.values[:, :-1]\n",
    "y_train = train_data.values[:, -1]\n",
    "X_val = validate_data.values[:, :-1]\n",
    "y_val = validate_data.values[:, -1]\n",
    "\n",
    "print(\"X_train sample:\", X_train[0])\n",
    "print(\"y_train sample:\", y_train[0])\n",
    "print(\"X_val sample:\", X_val[0])\n",
    "print(\"y_val sample:\", y_val[0])\n",
    "\n",
    "# Split the movie title to extract the shooting year as a feature\n",
    "# eg. Toy Story (1995) -> 1995 if the title has no year, use 1900 instead\n",
    "def get_year(title):\n",
    "    try:\n",
    "        return float(int(title.split(\"(\")[-1].split(\")\")[0].split(\"-\")[0]) / 2000.0)\n",
    "    except:\n",
    "        return 0.0\n",
    "    \n",
    "title_years_train = np.array(\n",
    "    [get_year(title) for title in X_train[:, 2]]\n",
    ")\n",
    "X_train = np.concatenate((X_train, title_years_train[:, np.newaxis]), axis=1)\n",
    "print(\"X_train sample:\", X_train[0])\n",
    "\n",
    "title_years_val = np.array(\n",
    "    [get_year(title) for title in X_val[:, 2]]\n",
    ")\n",
    "X_val = np.concatenate((X_val, title_years_val[:, np.newaxis]), axis=1)\n",
    "print(\"X_val sample:\", X_val[0])\n",
    "\n",
    "# Split the genres by \"|\" to make the genre as a sparse feature by\n",
    "# one-hot encoding\n",
    "genres_sparse_train = [genre.split(\"|\") for genre in X_train[:, 3]]\n",
    "\n",
    "# Concatenate the encoded genres with the existing features\n",
    "X_train = np.concatenate((X_train, MultiLabelBinarizer().fit_transform(genres_sparse_train)), axis=1)\n",
    "\n",
    "genres_sparse_val = [genre.split(\"|\") for genre in X_val[:, 3]]\n",
    "X_val = np.concatenate((X_val, MultiLabelBinarizer().fit_transform(genres_sparse_val)), axis=1)\n",
    "\n",
    "# Drop the title and genres columns and keep the encoded genres, convert the data to float32\n",
    "# [1 296 'Pulp Fiction (1994)' 'Comedy|Crime|Drama|Thriller' 0.997 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 1 0 0]\n",
    "width = X_train.shape[1]\n",
    "X_train = X_train[:, np.r_[0:2, 4:width-1]].astype(np.float32)\n",
    "X_val = X_val[:, np.r_[0:2, 4:width-1]].astype(np.float32)\n",
    "y_train = y_train.astype(np.float32)\n",
    "y_val = y_val.astype(np.float32)\n",
    "\n",
    "print(\"X_train sample:\", X_train[0])\n",
    "print(\"X_val sample:\", X_val[0])\n",
    "# Define the model architecture\n",
    "model = tf.keras.Sequential(\n",
    "    [\n",
    "        tf.keras.layers.Dense(64, activation=\"relu\"),\n",
    "        tf.keras.layers.Dense(32, activation=\"relu\"),\n",
    "        tf.keras.layers.Dense(1),\n",
    "    ]\n",
    ")\n",
    "\n",
    "# Compile the model\n",
    "model.compile(optimizer=\"adam\", loss=\"mse\")\n",
    "\n",
    "# Train the model with log history of loss\n",
    "history = model.fit(\n",
    "    X_train,\n",
    "    y_train,\n",
    "    validation_data=(X_val, y_val),\n",
    "    epochs=10,\n",
    "    batch_size=40960,\n",
    "    verbose=1,\n",
    ")\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Some plot of training history, and validation RMSE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "123/123 [==============================] - 0s 3ms/step\n",
      "RMSE: 7.247381\n"
     ]
    },
    {
     "data": {
      "image/png": "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RpcDtQT3cePG6fe//70kKTs7W4WFhXrhhRcIKAb461//qldffVVLlizRiBEjVFBQoNtvv10pKSn8PM4CAUVS3759ZbFYVF5e3u58eXm5kpKSDKoK8+bN0/Lly7Vu3TqlpqYaXU7Q2rx5syoqKnTuuef6zrndbq1bt07PPvusGhoaZLFYDKww+CQnJyszM7PdueHDh+v//u//DKoouN1111265557dO2110qSRo0apa+//lp5eXkElLNAD4qksLAwjR07VqtXr/ad83g8Wr16tXJycgysLDh5vV7NmzdPy5Yt04cffqgBAwYYXVJQmzRpkrZt26aCggLfMW7cOM2ePVsFBQWEEwNceOGF31l6v2fPHvXv39+gioJbXV2dzOb2v04tFos8Ho9BFfUOjKC0mj9/vubMmaNx48bp/PPP15NPPqna2lr96le/Mrq0oJObm6slS5bo7bffVkxMjMrKyiRJdrtdERERBlcXfGJiYr7T/xMVFaX4+Hj6ggxyxx136IILLtDvf/97/exnP9Nnn32ml156SS+99JLRpQWlK6+8Ur/73e+Unp6uESNGaMuWLXr88cf1T//0T0aXFti88HnmmWe86enp3rCwMO/555/v3bBhg9ElBSVJpzxefvllo0tDqx/96Efe2267zegygto777zjHTlypNdqtXozMjK8L730ktElBS2Xy+W97bbbvOnp6d7w8HDvwIEDvf/+7//ubWhoMLq0gMY+KAAAwO/QgwIAAPwOAQUAAPgdAgoAAPA7BBQAAOB3CCgAAMDvEFAAAIDfIaAAAAC/Q0ABAAB+h4ACAAD8DgEFAAD4HQIKAADwOwQUAADgd/4/ddy4c/yN+L4AAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Plot the loss and rmse\n",
    "import matplotlib.pyplot as plt\n",
    "plt.plot(history.history[\"val_loss\"])\n",
    "\n",
    "# Evaluate the model\n",
    "y_pred = model.predict(X_val, batch_size=40960)\n",
    "rmse = np.sqrt(mean_squared_error(y_val, y_pred))\n",
    "print(\"RMSE:\", rmse)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
